package com.koron.bean.util;

import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;


/**
 * @author idgen is composed of: time - 41 bits (millisecond precision w/ a custom
 * epoch gives us 69 years) configured machine idgen - 10 bits - gives us
 * up to 1024 machines sequence number - 12 bits - rolls over every 4096
 * per machine (with protection to avoid rollover in the same ms)
 */
public class DefaultIdGenerator implements IdGenerator {
    // idgen format =>
    // timestamp |datacenter | sequence
    // 41 |10 | 12
    private final long sequenceBits = 12;
    private final long datacenterIdBits = 10L;
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

    private final long datacenterIdShift = sequenceBits;
    private final long timestampLeftShift = sequenceBits + datacenterIdBits;

    private final long twepoch = 1288834974657L;
    private final long datacenterId;
    private final long sequenceMax = 4096;

    private volatile long lastTimestamp = -1L;
    private volatile long sequence = 0L;

    private static DefaultIdGenerator defaultIdGenerator;

    public static DefaultIdGenerator getInstance() throws GetHardwareIdFailedException {
        if (defaultIdGenerator == null) {
            defaultIdGenerator = new DefaultIdGenerator();
        }
        return defaultIdGenerator;
    }

    private DefaultIdGenerator() throws GetHardwareIdFailedException {
        datacenterId = getDatacenterId();
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new GetHardwareIdFailedException("datacenterId > maxDatacenterId");
        }
    }

    @Override
    public synchronized String generateLongId() throws InvalidSystemClockException {
        long timestamp = System.currentTimeMillis();
        if (timestamp < lastTimestamp) {
            throw new InvalidSystemClockException("Clock moved backwards.  Refusing to generate idgen for " + (
                    lastTimestamp - timestamp) + " milliseconds.");
        }
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) % sequenceMax;
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0;
        }
        lastTimestamp = timestamp;
        Long id = ((timestamp - twepoch) << timestampLeftShift) |
                (datacenterId << datacenterIdShift) |
                sequence;
        return id.toString();
    }

    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = System.currentTimeMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = System.currentTimeMillis();
        }
        return timestamp;
    }

    protected long getDatacenterId() throws GetHardwareIdFailedException {
        try {
            byte[] mac = null;
            Enumeration<NetworkInterface> ifs = NetworkInterface.getNetworkInterfaces();
            if (ifs != null) {
                while (ifs.hasMoreElements()) {
                    NetworkInterface iface = (NetworkInterface) ifs.nextElement();
                    mac = iface.getHardwareAddress();
                    if ((mac != null) && (mac.length == 6) && (mac[1] != -1)) {
                        break;
                    }
                }
            }

            long id;
            if (mac == null) {
                id = 1;
            } else {
                id = ((0x000000FF & (long) mac[mac.length - 1]) | (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
            }
            return id;
        } catch (SocketException e) {
            throw new GetHardwareIdFailedException(e);
        }
    }

    public static String getId() throws GetHardwareIdFailedException, InvalidSystemClockException {
        return DefaultIdGenerator.getInstance().generateLongId();
    }
}
